package com.adam.example.newssearches8_2_0.service;

import co.elastic.clients.elasticsearch.ElasticsearchClient;
import co.elastic.clients.elasticsearch._types.ErrorCause;
import co.elastic.clients.elasticsearch._types.Result;
import co.elastic.clients.elasticsearch.core.*;
import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem;
import co.elastic.clients.elasticsearch.core.search.Hit;
import co.elastic.clients.elasticsearch.core.search.HitsMetadata;
import co.elastic.clients.elasticsearch.indices.CreateIndexResponse;
import co.elastic.clients.elasticsearch.indices.DeleteIndexResponse;
import co.elastic.clients.elasticsearch.indices.ElasticsearchIndicesClient;
import co.elastic.clients.json.jackson.JacksonJsonpMapper;
import co.elastic.clients.transport.ElasticsearchTransport;
import co.elastic.clients.transport.endpoints.BooleanResponse;
import co.elastic.clients.transport.rest_client.RestClientTransport;
import com.adam.example.newssearches8_2_0.common.PagedData;
import com.adam.example.newssearches8_2_0.dto.NewsDTO;
import com.adam.example.newssearches8_2_0.dto.SearchNewsDTO;
import com.adam.example.newssearches8_2_0.po.NewsESO;
import com.adam.example.newssearches8_2_0.po.NewsPO;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.elasticsearch.client.RestClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.thymeleaf.util.StringUtils;

import javax.annotation.PostConstruct;
import java.io.IOException;
import java.time.format.DateTimeFormatter;
import java.util.*;

@Service
public class EsService implements DisposableBean {

    public static final String CLUSTER_NAME = "elasticsearch";
    public static final String HOST_IP = "192.168.0.111";
    public static final int TCP_PORT = 9300;
    public static final String INDEX_NAME = "test_spnews";
    public static final String TYPE_NAME = "news";
    public static final int NUMBER_OF_SHARDS = 3;
    public static final int NUMBER_OF_REPLICAS = 0;
    public static final String[] fields = {"title", "content"};
    public static final int PAGE_SIZE = 10;
    public static final String HIGHLIGHTER_PRE_TAG = "<span style=\"color:red\">";
    public static final String HIGHLIGHTER_POST_TAG = "</span>";
    public static final int CONTENT_TRIM_LENGTH = 200;

    private final Logger logger = LoggerFactory.getLogger(EsService.class);

    private ElasticsearchClient elasticsearchClient;

    @PostConstruct
    public void postConstruct() {
        try {
            final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
            credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(
                    "elastic", "elastic"));

            RestClient restClient = RestClient.builder(
                            new HttpHost("192.168.0.112", 9200))
                    .setHttpClientConfigCallback(httpAsyncClientBuilder -> httpAsyncClientBuilder
                            .setDefaultCredentialsProvider(credentialsProvider))
                    .build();

            ElasticsearchTransport elasticsearchTransport = new RestClientTransport(
                    restClient, new JacksonJsonpMapper());

            this.elasticsearchClient = new ElasticsearchClient(elasticsearchTransport);
        } catch (Exception e) {
            logger.error("建立ES连接失败", e);
        }
    }

    public ElasticsearchIndicesClient indicesClient() {
        if(elasticsearchClient != null) {
            return elasticsearchClient.indices();
        } else {
            return null;
        }
    }

    public boolean createNewsIndex() {
        ElasticsearchIndicesClient indicesClient = indicesClient();
        Assert.notNull(indicesClient, "indicesClient null");

        CreateIndexResponse createIndexResponse = null;
        try {
            createIndexResponse = indicesClient.create(builder -> builder
                    .index(INDEX_NAME)
                    .settings(indexSettingsBuilder -> indexSettingsBuilder
                            .numberOfShards(String.valueOf(NUMBER_OF_SHARDS))
                            .numberOfReplicas(String.valueOf(NUMBER_OF_REPLICAS))
                    )
                    .mappings(typeMappingBuilder -> typeMappingBuilder
                            .properties("id", propertyBuilder -> propertyBuilder
                                    .long_(longNumberPropertyBuilder -> longNumberPropertyBuilder)
                            )
                            .properties("title", propertyBuilder -> propertyBuilder
                                    .text(textPropertyBuilder -> textPropertyBuilder
                                            .analyzer("ik_max_word")
                                            .searchAnalyzer("ik_max_word")
//                                            .boost(2.0)  //不支持boost参数
                                    )
                            )
                            .properties("key_word", propertyBuilder -> propertyBuilder
                                    .text(textPropertyBuilder -> textPropertyBuilder
                                            .analyzer("ik_max_word")
                                            .searchAnalyzer("ik_max_word")
                                    )
                            )
                            .properties("content", propertyBuilder -> propertyBuilder
                                    .text(textPropertyBuilder -> textPropertyBuilder
                                            .analyzer("ik_max_word")
                                            .searchAnalyzer("ik_max_word")
                                    )
                            )
                            .properties("url", propertyBuilder -> propertyBuilder
                                    .keyword(keywordPropertyBuilder -> keywordPropertyBuilder)
                            )
                            .properties("reply", propertyBuilder -> propertyBuilder
                                    .long_(longNumberPropertyBuilder -> longNumberPropertyBuilder)
                            )
                            .properties("source", propertyBuilder -> propertyBuilder
                                    .keyword(keywordPropertyBuilder -> keywordPropertyBuilder)
                            )
                            .properties("postdate", propertyBuilder -> propertyBuilder
                                    .date(datePropertyBuilder -> datePropertyBuilder
                                            .format("yyyy-MM-dd HH:mm:ss")
                                    )
                            ))
            );
            return createIndexResponse.acknowledged();
        } catch (IOException e) {
            logger.error("创建索引失败", e);
            return false;
        }
    }

    public boolean newsIndexExists() {
        ElasticsearchIndicesClient indicesClient = indicesClient();
        Assert.notNull(indicesClient, "indicesClient null");

        try {
            BooleanResponse indicesExistsResponse = indicesClient.exists(builder -> builder
                    .index(INDEX_NAME)
            );
            return indicesExistsResponse.value();
        } catch (IOException e) {
            logger.error("查询索引是否存在失败", e);
            return false;
        }
    }

    public boolean deleteNewsIndex() {
        ElasticsearchIndicesClient indicesClient = indicesClient();
        Assert.notNull(indicesClient, "indicesClient null");

        try {
            DeleteIndexResponse deleteIndexResponse = indicesClient.delete(builder -> builder
                    .index(INDEX_NAME)
            );
            return deleteIndexResponse.acknowledged();
        } catch (IOException e) {
            logger.error("删除索引失败", e);
            return false;
        }
    }

    public boolean saveNews(List<NewsPO> newsPOList) {
        Assert.notNull(elasticsearchClient, "elasticsearchClient null");
        BulkRequest.Builder bulkRequestBuilder = new BulkRequest.Builder();
        newsPOList.forEach(newsPO -> {
            NewsESO newsESO = NewsESO.of(newsPO);
            bulkRequestBuilder.operations(builder -> builder
                    .index(indexOperationBuilder -> indexOperationBuilder
                            .index(INDEX_NAME)
                            .id(String.valueOf(newsESO.getId()))
                            .document(newsESO)
                    )
            );
        });
        BulkRequest bulkRequest = bulkRequestBuilder.build();
        try {
            BulkResponse bulkResponse = elasticsearchClient.bulk(bulkRequest);
            List<BulkResponseItem> bulkResponseItemList = bulkResponse.items();
            boolean returnResult = true;
            for (BulkResponseItem bulkResponseItem : bulkResponseItemList) {
                ErrorCause errorCause = bulkResponseItem.error();
                if (errorCause != null) {
                    logger.warn("Bulk请求失败，异常类型{}，原因{}", errorCause.type(), errorCause.reason());
                    returnResult = false;
                } else {
                    String result = bulkResponseItem.result();
                    if(result == null) {
                        logger.warn("bulk response no result");
                    } else if (result.equals(Result.Updated.jsonValue())) {
                        logger.warn("update existing news doc {}", bulkResponseItem.id());
                    } else if (!result.equals(Result.Created.jsonValue())) {
                        returnResult = false;
                        logger.warn("create news doc {} invalid result {}", bulkResponseItem.id(), result);
                    }
                }
            }
            return returnResult;
        } catch (IOException e) {
            logger.error("保存新闻到ES失败", e);
            return false;
        }
    }

    public long deleteAllNews() {
        Assert.notNull(elasticsearchClient, "elasticsearchClient null");
        DeleteByQueryResponse deleteByQueryResponse = null;
        try {
            deleteByQueryResponse = elasticsearchClient.deleteByQuery(builder -> builder
                    .index(INDEX_NAME)
                    .query(queryBuilder -> queryBuilder
                            .matchAll(matchAllQueryBuilder -> matchAllQueryBuilder)
                    )
            );
            Long deleted = deleteByQueryResponse.deleted();
            if(deleted == null) {
                logger.error("删除所有新闻失败，deleted返回null：{}", deleteByQueryResponse);
                return -1;
            }
            return deleted;
        } catch (IOException e) {
            logger.error("删除所有新闻失败", e);
            return -1;
        }
    }

    public SearchNewsDTO searchNews(String query, int pageNo) {
        Assert.notNull(elasticsearchClient, "elasticsearchClient null");

        List<NewsDTO> resultList = new LinkedList<>();
        if(StringUtils.isEmpty(query)) {
            return emptySearchNewsDTO(pageNo, resultList);
        }

        long start = System.currentTimeMillis();
        SearchRequest searchRequest = SearchRequest.of(builder -> builder
                .index(INDEX_NAME)
                .query(queryBuilder -> queryBuilder
                        .bool(boolQueryBuilder -> boolQueryBuilder
                                .should(queryBuilder1 -> queryBuilder1
                                        .matchPhrasePrefix(matchPhrasePrefixQueryBuilder -> matchPhrasePrefixQueryBuilder
                                                .field(fields[0]).query(query)
                                                .boost(2.0f)
                                        )
                                )
                                .should(queryBuilder1 -> queryBuilder1
                                        .matchPhrasePrefix(matchPhrasePrefixQueryBuilder -> matchPhrasePrefixQueryBuilder
                                                .field(fields[1]).query(query)
                                        )
                                )
                        )
                )
                .highlight(highlightBuilder -> highlightBuilder
                        .fields(fields[0], highlightFieldBuilder -> highlightFieldBuilder
                                .preTags(HIGHLIGHTER_PRE_TAG).postTags(HIGHLIGHTER_POST_TAG)
                        )
                        .fields(fields[1], highlightFieldBuilder -> highlightFieldBuilder
                                .preTags(HIGHLIGHTER_PRE_TAG).postTags(HIGHLIGHTER_POST_TAG)
                        )
                )
                .from((pageNo - 1) * PAGE_SIZE)
                .size(PAGE_SIZE)
        );
        try {
            SearchResponse<NewsESO> searchResponse = elasticsearchClient.search(searchRequest, NewsESO.class);
            HitsMetadata<NewsESO> hitsMetadata = searchResponse.hits();
            List<Hit<NewsESO>> newsESOHitList = hitsMetadata.hits();
            for (Hit<NewsESO> newsESOHit : newsESOHitList) {
                NewsESO newsESO = newsESOHit.source();
                if (newsESO == null) {
                    continue;
                }
                NewsDTO newsDTO = NewsDTO.of(newsESO, newsESOHit.score());

                Map<String, List<String>> highlightMap = newsESOHit.highlight();
                List<String> highlightListTitle = highlightMap.get(fields[0]),
                        highlightListContent = highlightMap.get(fields[1]);
                if (!CollectionUtils.isEmpty(highlightListTitle)) {
                    StringBuilder hTitleStr = new StringBuilder();
                    for (String highlight : highlightListTitle) {
                        hTitleStr.append(highlight);
                    }
                    newsDTO.setTitle(hTitleStr.toString());
                }
                if (!CollectionUtils.isEmpty(highlightListContent)) {
                    StringBuilder hContentStrBuilder = new StringBuilder();
                    for (String highlight : highlightListContent) {
                        hContentStrBuilder.append(highlight).append("...");
                    }
                    for (int i = 0; i < 3; i++) {
                        hContentStrBuilder.deleteCharAt(hContentStrBuilder.length() - 1);
                    }
                    String hContentStr = hContentStrBuilder.toString();
                    if (hContentStr.length() > CONTENT_TRIM_LENGTH) {
                        int index1 = hContentStr.indexOf(HIGHLIGHTER_POST_TAG, CONTENT_TRIM_LENGTH - HIGHLIGHTER_POST_TAG.length()),
                                index2 = hContentStr.indexOf(HIGHLIGHTER_PRE_TAG, CONTENT_TRIM_LENGTH - HIGHLIGHTER_PRE_TAG.length());
                        if (index1 != -1 && index1 < CONTENT_TRIM_LENGTH) {
                            hContentStr = hContentStr.substring(0, index1) + "</span>";
                        } else if (index2 != -1 && index2 < CONTENT_TRIM_LENGTH) {
                            hContentStr = hContentStr.substring(0, index2);
                        } else {
                            hContentStr = hContentStr.substring(0, CONTENT_TRIM_LENGTH);
                        }
                        logger.debug("hContentStr {}", hContentStr);
                    }
                    newsDTO.setContent(hContentStr);
                }

                resultList.add(newsDTO);
            }

            PagedData<NewsDTO> pagedData = PagedData.of(pageNo, PAGE_SIZE, hitsMetadata.total().value(), resultList);
            long end = System.currentTimeMillis();

            return SearchNewsDTO.builder()
                    .pagedData(pagedData)
                    .costTime(1.0 * (end - start) / 1000)
                    .build();
        } catch (IOException e) {
            logger.error("搜索新闻失败", e);
            return emptySearchNewsDTO(pageNo, resultList);
        }
    }

    private SearchNewsDTO emptySearchNewsDTO(int pageNo, List<NewsDTO> newsDTOList) {
        if(newsDTOList == null) {
            newsDTOList = new ArrayList<>();
        }
        PagedData<NewsDTO> pagedData = PagedData.of(pageNo, PAGE_SIZE, 0, newsDTOList);
        return SearchNewsDTO.builder()
                .pagedData(pagedData)
                .costTime(0)
                .build();
    }

    @Override
    public void destroy() throws Exception {
        if(this.elasticsearchClient != null) {
            this.elasticsearchClient._transport().close();
        }
    }
}